package com.hhoss.conf;

import com.hhoss.bean.HMap;
import com.hhoss.boot.App;
import com.hhoss.logs.Logger;
import com.hhoss.util.Enumerations;
import com.hhoss.util.token.TokenParser;
import com.hhoss.util.token.TokenProvider;
import org.apache.commons.beanutils.ConvertUtils;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;


/**
 * res tree node, as a holder / bucket
 * @author kejun
 * old name PropsNode
 */
public final class ResHolder extends Properties {
	private static final long serialVersionUID = 1359257876422093067L;
	private static final Logger logger = Logger.get();
	private static final String LOCALE ="locale";
	private static final String CACHER ="cacher";
	private static final String HASHER ="hasher";
	private static final String PARENT ="parent";

	protected static final String PARENT_NAME = "res.parent.name";
	protected static final String LOCATE_NAME = "res.locate.name";// the res unique name 
	protected static final String LOCATE_PATH = "res.locate.path";// the res real full path 
	protected static final String LOCATE_RSID = "res.locate.rsid";// the resource id, refer self id
	
	/** define the value split delimiters for getList, if not set use default DEFAULT_DELIMS=",;| \t\r\n" */
	private static final String KEY_FOR_DELIMS="var.list.split.delimiters";
	private static final String DEFAULT_DELIMS=",;| \t\r\n";

	private static long msid=0;
	private String name;
	
	//the piority order: locale> holder> parent
	private String[] lookups = {LOCALE,CACHER,HASHER,PARENT,"appenv","system"};
	private final HMap<TokenProvider> providers = new HMap<>();

	public ResHolder(String name){setName(name);}
	
	public ResHolder(String name, Properties parentProps) {
		setFather(parentProps);
		setName(name);
	}

	private TokenProvider getProvider(String name){
		if( providers.get(name)==null ){
			providers.put(name, TokenProvider.from(name, PARENT.equals(name)?defaults:this));
		}
		return providers.get(name);
	}
	/*
	 * the res bundle unique name 
	 */
	public String getName(){
		return name==null?getProvider(HASHER).get(LOCATE_NAME):name;
	}

	/*
	 *  the bundle meta sid, myself sid
	 */
	public long getRsid(){		
		if(msid>0 || msid<-3){//has get msid ok, or failure 3 times;
			return msid;
		}		
		String s= getProvider(HASHER).get(LOCATE_RSID);
		if(s==null||s.trim().length()==0){
			msid--;
		}else try{
			msid = Long.parseLong(s);
		}catch(NumberFormatException e){
			logger.debug("bundle sid setting[{}={}] error.",LOCATE_RSID,s);
			msid=-9; //has value,but wrong directly; 
		}
		return msid;
	}

	protected void setName(String name){
		this.name=name;
	    setProperty(LOCATE_NAME,name);
	}

	protected void setFather(Properties parentProps) {
		this.defaults = parentProps;
		if (parentProps != null) {
			String parentName = (parentProps instanceof ResHolder)?	((ResHolder)parentProps).getName()
							  :(String)parentProps.get(LOCATE_NAME);
			if (parentName != null){
				setProperty(PARENT_NAME, parentName);
			}
		}
	}
	
	/**
	 * case insensitive <br />
	 * true if explicit set as list of tokens: eg. 1,Y,true,allow...  <br />
	 * false if not set or not the explicit values. <br />
	 * can set the tokens by var.token.true.values=...
	 * @param key
	 * @return boolean
	 */
	public boolean isTrue(String key) {
		String val= getProperty(key);
		if(val==null||(val=val.trim()).isEmpty()){return false;}
		String[] tks = getArray("var.token.true.values"); //TODO:cache ?
		if( tks.length==0 ){
			tks = "1,Y,T,A,O,S,YES,TRUE,ALLOW,OK,SUCCESS".split(",");
		}			
		for(String tk:tks){
			if(val.equalsIgnoreCase(tk)){return true;}
		}
		return false;
	}

	/**
	 * case insensitive <br />
	 * true if explicit set as list of tokens: eg. 0,N,F,false,deny....  <br />
	 * false if not set or not the explicit values.
	 * can set the tokens by var.token.false.values=...
	 * @param key
	 * @return boolean
	 */
	public boolean isFalse(String key) {
		String val= getProperty(key);
		if(val==null||(val=val.trim()).isEmpty()){return false;}
		String[] tks = getArray("var.token.false.values"); //TODO:cache ?
		if( tks.length==0 ){
			tks = "0,N,F,D,C,W,-1,NO,FALSE,DENY,CANCEL,NULL,FAIL,FAILURE,WRONG".split(",");
		}
		for(String tk:tks){
			if(val.equalsIgnoreCase(tk)){return true;}
		}
		return false;
	}
	
	/**
	 * true when the value is not set or set blank
	 * @param key
	 * @return bool
	 */
	public boolean isEmpty(String key) {
		String val= getProperty(key);
		return val==null||val.trim().isEmpty();
	}

	/**
     * Accepts decimal, hexadecimal, and octal numbers given by the following grammar:
     * <blockquote>
     * <dl>
     * <dt><i>DecodableString:</i>
     * <dd><i>Sign<sub>opt</sub> DecimalNumeral</i>
     * <dd><i>Sign<sub>opt</sub></i> {@code 0x} <i>HexDigits</i>
     * <dd><i>Sign<sub>opt</sub></i> {@code 0X} <i>HexDigits</i>
     * <dd><i>Sign<sub>opt</sub></i> {@code #} <i>HexDigits</i>
     * <dd><i>Sign<sub>opt</sub></i> {@code 0} <i>OctalDigits</i>
     * <p>
     * <dt><i>Sign:</i>
     * <dd>{@code -}
     * <dd>{@code +}
     * </dl>
     * </blockquote>
	 * @param key of the prop
	 * @return Long
	 */
	public Long getLong(String key) {
		String nm= getProperty(key);
		return (nm==null||nm.trim().length()==0)?null:Long.decode(nm);
	}
	/**
     * Accepts decimal, hexadecimal, and octal numbers given by the following grammar:
     * <blockquote>
     * <dl>
     * <dt><i>DecodableString:</i>
     * <dd><i>Sign<sub>opt</sub> DecimalNumeral</i>
     * <dd><i>Sign<sub>opt</sub></i> {@code 0x} <i>HexDigits</i>
     * <dd><i>Sign<sub>opt</sub></i> {@code 0X} <i>HexDigits</i>
     * <dd><i>Sign<sub>opt</sub></i> {@code #} <i>HexDigits</i>
     * <dd><i>Sign<sub>opt</sub></i> {@code 0} <i>OctalDigits</i>
     * <p>
     * <dt><i>Sign:</i>
     * <dd>{@code -}
     * <dd>{@code +}
     * </dl>
     * </blockquote>
	 * @param key of the prop
	 * @return Integer
	 */
	public Integer getInt(String key) {
		String nm= getProperty(key);
		return (nm==null||nm.trim().length()==0)?null:Integer.decode(nm);
	}
	/**
	 * support radix, 0x...
     * <dl>
     * <dt><i>HexSignificand:</i>
     * <dd>{@code 0x} <i>HexDigits<sub>opt</sub>
     *     </i>{@code .}<i> HexDigits</i>
     * <dd>{@code 0X}<i> HexDigits<sub>opt</sub>
     *     </i>{@code .} <i>HexDigits</i>
     * </dl>
	 * @param key of the prop
	 * @return float
	 * @see Double#valueOf(String s)
	 */
	public float getFloat(String key) {
		String nm= getProperty(key);
		return (nm==null)?null:Float.parseFloat(nm);
	}
	/**
	 * support radix, 0x...
     * <dl>
     * <dt><i>HexSignificand:</i>
     * <dd>{@code 0x} <i>HexDigits<sub>opt</sub>
     *     </i>{@code .}<i> HexDigits</i>
     * <dd>{@code 0X}<i> HexDigits<sub>opt</sub>
     *     </i>{@code .} <i>HexDigits</i>
     * </dl>
	 * @param key of the prop
	 * @return double
	 * @see Double#valueOf(String s)
	 */
	public double getDouble(String key) {
		String nm= getProperty(key);
		return (nm==null)?null:Double.parseDouble(nm);
	}

	/**
	 * the value as list by delimiters "var.list.split.delimiters" default as ",;| \t\n" <br />
	 * or values which key matched prefix{[.|_]?\d+} <br />
	 * the max matches entry is 1000 for prefix; <br />
	 * value item only support 10 radix, not support 16 radix
	 * @param key the key of property or prefix of property Array
	 * @param klass the class of element type
	 * @return List list is not null and each item is not null
	 */
	@SuppressWarnings("unchecked")
	public <T> List<T> getList(String key, Class<T> klass) {
		List<T> list = new ArrayList<>();
		for(String s:getList(key)){
			list.add((T) ConvertUtils.convert(s, klass));
		}
		return list;
	}
	
	/**
	 * the value as list by delimiters "var.list.split.delimiters" default as ",;| \t\n" <br />
	 * or values which key matched prefix{[.|_]?\d+} <br />
	 * the max matches entry is 1000 for prefix; <br />
	 * value item only support 10 radix, not support 16 radix
	 * @param key the key of property or prefix of property Array
	 * @return List list is not null and each item is not null
	 * @see org.springframework.context.ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS=",; \t\n"
	 */
	public List<String> getList(String key) {
		if(key==null||key.trim().isEmpty()){
			throw new IllegalArgumentException("Property key can't be empty.");
		}		
		List<String> list = new ArrayList<>();
		String val = getProperty(key);
		int max = 0;
		if(val!=null&&(val=val.trim()).length()>0){
			if(val.matches("#\\d+")){
				max=Integer.parseInt(val.substring(1));
			}else{
				String delims=getProperty(KEY_FOR_DELIMS, DEFAULT_DELIMS);
				StringTokenizer st = new StringTokenizer(val,delims);
				while(st.hasMoreTokens()){
					String v = st.nextToken();
					if(v!=null){list.add(v);}
				}
			}
		}
		if(list.isEmpty()){
			if(max==0){max=999;}
			String prefix = normalize(key);
			for(int i=0;i<=max;i++){
				String v = getProperty(prefix+i);
				if(i>0&&v==null){break;}
				if(v!=null){list.add(v);}
			}
		}
		return list;
	}

	/**
	 * @param key
	 * @return the array of config, allow null/empty item
	 */
	public String[] getArray(String key){
		if(key==null||key.trim().isEmpty()){
			throw new IllegalArgumentException("Property key can't be empty.");
		}		
		String val = getProperty(key);
		String[] values = null;
		if(val==null||(val=val.trim()).length()==0){
			List<String> list = new ArrayList<>();
			String prefix = normalize(key);
			String vi = getProperty(prefix+0);
			if(vi!=null){list.add(vi);}
			for(int i=1;i<999;i++){
				vi = getProperty(prefix+i);
				if(vi==null){break;}else{list.add(vi);}
			}
			values = list.toArray(new String[list.size()]);
		}else if(val.matches("#\\d+")){
			int max=Integer.parseInt(val.substring(1));
			String prefix = normalize(key);
			List<String> list = new ArrayList<>();
			for(int i=0;i<=max&&i<999;i++){
				list.add(getProperty(prefix+i));
			}
			values = list.toArray(new String[list.size()]);
		}else{
			String delims=getProperty(KEY_FOR_DELIMS, DEFAULT_DELIMS);
			values = val.split("["+delims+"]"); 
		}
		return values;
	}
	
	private static String normalize(String prefix){
		if(prefix.endsWith(".")||prefix.endsWith("_")){
			return prefix;
		}
		return prefix+=".";
	}


	/**
	 * when use (Map)get(), we still return the value from HashTable, don't
	 * recurse and replace, so comment; public synchronized String get(Object
	 * key) { return getProperty( String.valueOf(key) ); }
	 */

	/**
	 * @override
	 */
	@Override
	public synchronized Object put(Object key, Object val) {
		if( key==null||val==null ){
			logger.warn("ignore put property: {}={}",key,val);
			return val;
		}
		return (String) super.put(key,val);
	}
	/**
	 * @override
	 */
	@Override
	public String getProperty(String key) {
		return getProperty(key, true);
	}

	/**
	 * @param qkey
	 * @param subst need subst if not provider this parameter means true
	 *            to replace the holder of ${...} with substitution;
	 * @return val 
	 */
	public String getProperty(String qkey, boolean subst) {
		String key = getParser().parse(qkey);		
		String val = getOriginal(key);
		return (subst && val!=null)?convert(key,val):val;		
	}
	
	private String convert(String key, String val) {
		if( key.startsWith("crypto.")||
			key.endsWith(".secret")||
			key.endsWith(".cipher")||
			val.startsWith("$crypto$")||
			val.startsWith("$cipher$")||
			val.startsWith("$secret$")) {
			val = TokenProvider.from("cipher","").get(val);
		}
		return getParser().parse(val);
	}
	
	private TokenParser parser;
	private TokenParser getParser(){
		if( parser==null ){
			parser = TokenParser.get(this,true);
		}
		return parser;
	}

	protected final String getOriginal(String key) {
		//checkLocale();
		String v;
		TokenProvider tp;
		for(String lp:lookups){
			if( (tp=getProvider(lp))!=null && (v=tp.get(key))!=null ){
				logger.trace("in [{}/{}] -> [{}={}]",getName(),lp,key,v);
				return v;
			}
		}
		return null;
	}

	/**
	 * Load specified properties file and construct it as Properties
	 * 
	 * @param ResourceBundle
	 *            bundle
	 * @return Properties
	 */
	protected synchronized ResHolder load(ResourceBundle bundle) {
		Enumeration<String> keyList = bundle.getKeys();
		String keyName = null;
		while (keyList.hasMoreElements()) {
			keyName = keyList.nextElement();
			put(keyName, bundle.getString(keyName));
		}
		return this;
	}

	/**
	 * @override save properties with sorted order.
	 */
	@Override
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public synchronized void store(OutputStream out, String comments) throws IOException {
		TreeMap map = new TreeMap(this);
		BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
		if (comments != null)
			bw.write("#" + comments);
		bw.newLine();
		bw.write("#Saved by ELProperties: " + new Date().toString());
		bw.newLine();
		for(Entry<String,String> ent:(Set<Entry>)map.entrySet()){
			bw.write((String)ent.getKey());
			bw.write("=");
			bw.write((String)ent.getValue());
			bw.newLine();
		}
		bw.flush();
	}
	
	public void store(String comments) throws IOException {
		String name = getName().replace('.',File.separatorChar)+".properties";
		String path = getProperty(App.APP_RUNTIME_CONF);//ResLoader.get("");
		File file = new File(path,name);
		file.getParentFile().mkdirs();//.createNewFile();
		OutputStream out = new FileOutputStream(file);
		store(out,comments);
		out.close();
	}
	
	@Override
    public Enumeration<String> propertyNames() {
    	return Enumerations.of(stringPropertyNames());
    }
    
	@Override
    public Set<String> stringPropertyNames() {
    	Set<String> set = super.stringPropertyNames();
    	TokenProvider tp = getProvider(LOCALE);
    	Set<String> loc;
    	if(tp!=null && (loc=tp.keySet())!=null && !loc.isEmpty()){
    		set = new HashSet<String>(set);// super set can't modify;
    		set.addAll(loc);
    	}
        return set;
    }
	
	/**
	 * @param prefix
	 * @return properties which include the sub keys
	 */
	public Properties subHolder(String prefix){
		if(prefix==null||prefix.length()<1){return null;}
		String head=prefix.endsWith(".")?prefix:prefix+".";
		Properties sub= new ResHolder(this.getName()+"-"+prefix,this);
		for(String name : stringPropertyNames()){
			if(name.startsWith(head)&&name.length()>head.length()){
				sub.setProperty(name.substring(head.length()), "${"+name+"}");
//				sub.setProperty(name.substring(head.length()), getProperty(name));
			}
		}
		return sub;
	}
}
